/* ===========================================================
 * TradeManager : a application to trade strategies for the Java(tm) platform
 * ===========================================================
 *
 * (C) Copyright 2011-2011, by Simon Allen and Contributors.
 *
 * Project Info:  org.trade
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
 * License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * [Java is a trademark or registered trademark of Oracle, Inc.
 * in the United States and other countries.]
 *
 * (C) Copyright 2011-2011, by Simon Allen and Contributors.
 *
 * Original Author:  Simon Allen;
 * Contributor(s):   -;
 *
 * Changes
 * -------
 *
 */
package org.trade.core.valuetype;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Comparator;

import org.trade.core.conversion.JavaTypeTranslator;
import org.trade.core.message.IMessageFactory;
import org.trade.core.util.CoreUtils;
import org.trade.core.validator.DecimalValidator;
import org.trade.core.validator.ExceptionMessageListener;
import org.trade.core.validator.Validator;

/**
 */
public class Decimal extends ValueType implements Comparator<Decimal>,
		Comparable<Decimal> {
	private static final long serialVersionUID = 4937298768811778585L;

	public final static Decimal ZERO = new Decimal(0L, 0);

	protected static Boolean m_ascending = new Boolean(true);

	static {
		// Register the appropriate converters
		JavaTypeTranslator.registerDynamicTypeConverter(new ObjectToDecimal());
		JavaTypeTranslator.registerDynamicTypeConverter(new DecimalToObject());
	}

	private static String MULTIPLIER = "1";
	public static String DECIMAL_POSITIVE_7_SCALE = "#######";
	public static String DECIMAL_NONNEGATIVE_8_SCALE = "########";
	public static String DECIMAL_POSITIVE_10_SCALE = "##########";
	public static String DECIMAL_NONNEGATIVE_11_SCALE = "###########";
	private static int SCALE = 0;
	private String m_format = DECIMAL_NONNEGATIVE_11_SCALE;
	private BigDecimal m_value = null;
	private String m_invalidValue = null; // This will be null if there were

	/**
	 * Default Constructor. Create an object and initialize it to empty.
	 */
	public Decimal(int scale) {

		SCALE = scale;
		MULTIPLIER = String.valueOf(Math.pow(10, scale));
		if (scale > 0) {
			DECIMAL_POSITIVE_7_SCALE = "#######.";
			DECIMAL_NONNEGATIVE_8_SCALE = "########.";
			DECIMAL_POSITIVE_10_SCALE = "##########.";
			DECIMAL_NONNEGATIVE_11_SCALE = "###########.";
			for (int i = 0; i < scale; i++) {
				DECIMAL_POSITIVE_7_SCALE = DECIMAL_POSITIVE_7_SCALE + "#";
				DECIMAL_NONNEGATIVE_8_SCALE = DECIMAL_NONNEGATIVE_8_SCALE + "#";
				DECIMAL_POSITIVE_10_SCALE = DECIMAL_POSITIVE_10_SCALE + "#";
				DECIMAL_NONNEGATIVE_11_SCALE = DECIMAL_NONNEGATIVE_11_SCALE
						+ "#";
			}
		}
	}

	/**
	 * Default Constructor. Create an object and initialize it to empty.
	 * 
	 * @param deciamlString
	 *            String
	 */
	public Decimal(String deciamlString, int scale) {
		this(scale);
		if ((null != deciamlString) && (deciamlString.length() != 0)) {
			// This is necessary because Java will parse strings with multiple
			// dashes
			if (deciamlString.indexOf("-") != deciamlString.lastIndexOf("-")) {
				m_invalidValue = deciamlString;
			} else {
				try {
					setBigDecimal(new BigDecimal(deciamlString));
				} catch (NumberFormatException e) {
					m_invalidValue = deciamlString;
				}
			}
		}
	}

	/**
	 * Constructor
	 * 
	 * 
	 * @param d
	 *            double
	 */
	public Decimal(double d, int scale) {
		this(scale);
		setBigDecimal(new BigDecimal(d));
	}

	/**
	 * Constructor
	 * 
	 * 
	 * @param d
	 *            Double
	 */
	public Decimal(Double d, int scale) {
		this(scale);
		setBigDecimal(new BigDecimal(d.doubleValue()));
	}

	/**
	 * Constructor
	 * 
	 * 
	 * @param bd
	 *            BigDecimal
	 */
	public Decimal(BigDecimal bd, int scale) {
		this(scale);
		setBigDecimal(bd);
	}

	/**
	 * Constructor for Decimal.
	 * 
	 * @param decimal
	 *            Decimal
	 */
	public Decimal(Decimal decimal, int scale) {
		this(scale);
		m_value = decimal.m_value;
		m_format = decimal.m_format;
		m_invalidValue = decimal.m_invalidValue;
	}

	/**
	 * Constructor
	 * 
	 * 
	 * 
	 * @param nonDecimalAmount
	 *            long
	 * @param decimalAmount
	 *            int
	 */
	public Decimal(long nonDecimalAmount, int decimalAmount, int scale) {
		this(scale);
		// Set up the default constraints for IP's basic Money values
		BigDecimal val = new BigDecimal((nonDecimalAmount * 100)
				+ decimalAmount);
		setBigDecimal(val.movePointLeft(SCALE));
	}

	/**
	 * Provides the format used for determining if this object is valid. The
	 * format should be one of the format constants on this class. The default
	 * format is NORMAL_11_2.
	 * 
	 * @param format
	 *            String
	 */
	public void setFormat(String format) {
		m_format = format;
	}

	/**
	 * Method getFormat.
	 * 
	 * @return String
	 */
	public String getFormat() {
		return m_format;
	}

	/**
	 * This maximum length includes the decimal point and digits to both sides.
	 * 
	 * @return int
	 */
	public int getMaxLength() {
		int maxLength = 14;
		if (getFormat().equals(DECIMAL_NONNEGATIVE_8_SCALE)) {
			maxLength = DECIMAL_NONNEGATIVE_8_SCALE.length();
		} else if (getFormat().equals(DECIMAL_POSITIVE_10_SCALE)) {
			maxLength = DECIMAL_POSITIVE_10_SCALE.length();
		} else if (getFormat().equals(DECIMAL_POSITIVE_7_SCALE)) {
			maxLength = DECIMAL_POSITIVE_7_SCALE.length();
		}
		return maxLength;
	}

	/**
	 * This indicates whether zero is an acceptable value for this instance.
	 * Currently this is determined by the format returned by getFormat().
	 * 
	 * @return boolean
	 */
	public boolean canBeZero() {
		boolean zero = true;
		if (getFormat().equals(DECIMAL_POSITIVE_7_SCALE)) {
			zero = false;
		} else if (getFormat().equals(DECIMAL_POSITIVE_10_SCALE)) {
			zero = false;
		}
		return zero;
	}

	/**
	 * This indicates whether zero is an acceptable value for this instance.
	 * Currently this is determined by the format returned by getFormat().
	 * 
	 * @return boolean
	 */
	public boolean canBeNegative() {
		boolean negative = true;
		return negative;
	}

	/**
	 * Method isNegative.
	 * 
	 * @return boolean
	 */
	public boolean isNegative() {
		assertDefined();
		boolean negative = false;
		if (m_value.compareTo(new BigDecimal(0)) < 0) {
			negative = true;
		}
		return negative;
	}

	/**
	 * Method isEmpty.
	 * 
	 * @return boolean
	 */
	public boolean isEmpty() {
		boolean empty = false;

		if ((null == m_value) || (null != m_invalidValue)) {
			empty = true;
		}

		return empty;
	}

	/**
	 * 
	 * @return The value before the decimal point in the money value.
	 */
	public long getNonDecimalAmount() {
		assertDefined();

		long nonDecimalAmount = 0;

		if (null != m_value) {
			nonDecimalAmount = m_value.longValue();
		}

		return nonDecimalAmount;
	}

	/**
	 * See description of superclass method. Overrode functionality to return
	 * the BigDecimal this object is using intrnally.
	 * 
	 * @return Object
	 */
	public Object getSQLObject() {
		return (getBigDecimalValue());
	}

	/**
	 * 
	 * @return The value after the decimal point in the money value.
	 */
	public int getDecimalAmount() {
		assertDefined();

		int decimalAmount = 0;

		if (null != m_value) {
			BigInteger tot = (m_value.movePointRight(SCALE)).toBigInteger();
			BigInteger sub = m_value.toBigInteger();
			sub = sub.multiply(new BigInteger(MULTIPLIER));

			BigInteger res = tot.subtract(sub);

			decimalAmount = res.intValue();
		}

		return decimalAmount;
	}

	/**
	 * Will throw a <code>NullPointerException</code> if this valuetype is
	 * empty.
	 * 
	 * 
	 * @return A BigDecimal representing the monetary value.
	 */
	public BigDecimal getBigDecimalValue() {
		assertDefined();

		return m_value;
	}

	/**
	 * Method toString.
	 * 
	 * @return String
	 */
	public String toString() {
		if (null != m_value) {
			return (m_value.toString());
		} else if (null != m_invalidValue) {
			return m_invalidValue;
		} else {
			return "";
		}
	}

	/**
	 * Method setValue.
	 * 
	 * @param value
	 *            Object
	 * @throws ValueTypeException
	 */
	public void setValue(Object value) throws ValueTypeException {
		if (value instanceof Decimal) {
			setBigDecimal(((Decimal) value).m_value);
		} else {
			try {
				setBigDecimal(((Decimal) JavaTypeTranslator.convert(
						Decimal.class, value)).getBigDecimalValue());
			} catch (Exception ex) {
				throw new ValueTypeException(ex);
			}
		}
	}

	/**
	 * Adds two Money objects
	 * 
	 * 
	 * 
	 * 
	 * @param decimal
	 *            Decimal
	 * @return Money the result
	 */
	public Decimal add(Decimal decimal) {
		assertDefined();

		if (null == m_value) {
			if (null == decimal.getBigDecimalValue()) {
				return new Decimal(SCALE);
			} else {
				return new Decimal(decimal.getBigDecimalValue(), SCALE);
			}
		}

		BigDecimal value = m_value.add(decimal.getBigDecimalValue());
		return new Decimal(value, SCALE);
	}

	/**
	 * Subtracts two decimal objects
	 * 
	 * 
	 * 
	 * 
	 * @param decimal
	 *            Decimal
	 * @return Money the result
	 */
	public Decimal subtract(Decimal decimal) {
		assertDefined();

		if (null == m_value) {
			return (decimal);
		}

		BigDecimal value = m_value.subtract(decimal.getBigDecimalValue());
		return new Decimal(value, SCALE);
	}

	/**
	 * Compares two Money objects.
	 * 
	 * 
	 * 
	 * 
	 * @param decimal
	 *            Decimal
	 * @return boolean result.
	 */
	public boolean isLessThan(Decimal decimal) {
		assertDefined();

		BigDecimal thisValue = notNull(this);
		BigDecimal parameter = notNull(decimal);

		return (thisValue.compareTo(parameter) < 0);
	}

	/**
	 * Compares two Money objects.
	 * 
	 * 
	 * 
	 * 
	 * @param decimal
	 *            Decimal
	 * @return boolean result.
	 */
	public boolean isLessThanOrEqualTo(Decimal decimal) {
		assertDefined();

		BigDecimal thisValue = notNull(this);
		BigDecimal parameter = notNull(decimal);

		return (thisValue.compareTo(parameter) <= 0);
	}

	/**
	 * Compares two Money objects.
	 * 
	 * 
	 * 
	 * 
	 * @param decimal
	 *            Decimal
	 * @return boolean result.
	 */
	public boolean isGreaterThan(Decimal decimal) {
		assertDefined();

		BigDecimal thisValue = notNull(this);
		BigDecimal parameter = notNull(decimal);

		return (thisValue.compareTo(parameter) > 0);
	}

	/**
	 * Compares two Money objects.
	 * 
	 * 
	 * 
	 * 
	 * @param decimal
	 *            Decimal
	 * @return boolean result.
	 */
	public boolean isGreaterThanOrEqualTo(Decimal decimal) {
		assertDefined();

		BigDecimal thisValue = notNull(this);
		BigDecimal parameter = notNull(decimal);

		return (thisValue.compareTo(parameter) >= 0);
	}

	/**
	 * Method isValid.
	 * 
	 * @param validator
	 *            Validator
	 * @param receiver
	 *            ExceptionMessageListener
	 * @return boolean
	 */
	public boolean isValid(Validator validator,
			ExceptionMessageListener receiver) {
		return validator.isValid(m_value, m_invalidValue, null, receiver);
	}

	/**
	 * Method getDefaultValidator.
	 * 
	 * @param messageFactory
	 *            IMessageFactory
	 * @param isMandatory
	 *            boolean
	 * @return Validator
	 */
	public Validator getDefaultValidator(IMessageFactory messageFactory,
			boolean isMandatory) {
		// This allow non-negative 11.2
		return new DecimalValidator(messageFactory, false, true, 11, 2,
				isMandatory);
	}

	/**
	 * Will throw a <code>NullPointerException</code> if this valuetype is
	 * empty.
	 * 
	 * 
	 * @return A double representing the monetary value.
	 */

	public double doubleValue() {
		assertDefined();

		return m_value.doubleValue();
	}

	/**
	 * Overrides Cloneable
	 * 
	 * 
	 * 
	 * 
	 * 
	 * 
	 * @return Object
	 * @exception * @see
	 */

	public Object clone() {
		try {
			Decimal other = (Decimal) super.clone();
			return other;
		} catch (CloneNotSupportedException e) {
			// will never happen
			return null;
		}
	}

	/**
	 * Method compareTo.
	 * 
	 * @param other
	 *            Decimal
	 * @return int
	 */
	public int compareTo(final Decimal other) {
		return CoreUtils.nullSafeComparator(this.getBigDecimalValue(),
				other.getBigDecimalValue());
	}

	/**
	 * Method compare.
	 * 
	 * @param o1
	 *            Decimal
	 * @param o2
	 *            Decimal
	 * @return int
	 */
	public int compare(Decimal o1, Decimal o2) {

		int returnVal = CoreUtils.nullSafeComparator(o1.getBigDecimalValue(),
				o2.getBigDecimalValue());
		if (m_ascending.equals(Boolean.FALSE)) {
			returnVal = returnVal * -1;
		}
		return returnVal;
	}

	/**
	 * Method equals.
	 * 
	 * @param objectToCompare
	 *            Object
	 * @return boolean
	 */
	public boolean equals(Object objectToCompare) {

		if (super.equals(objectToCompare))
			return true;

		if (objectToCompare instanceof Decimal) {
			if (CoreUtils.nullSafeComparator(
					((Decimal) objectToCompare).getBigDecimalValue(),
					this.getBigDecimalValue()) == 0)
				return true;
		}
		return false;
	}

	//
	// Private Methods
	//

	/**
	 * Method setBigDecimal.
	 * 
	 * @param value
	 *            BigDecimal
	 */
	private void setBigDecimal(BigDecimal value) {
		if (value == null) {
			m_value = new BigDecimal(0.0);
		} else {
			// m_value = value;
			m_value = value.setScale(SCALE, BigDecimal.ROUND_HALF_EVEN);
		}

		// Clear any invalid values
		m_invalidValue = null;
	}

	/**
	 * Method notNull.
	 * 
	 * @param value
	 *            Decimal
	 * @return BigDecimal
	 */
	private BigDecimal notNull(Decimal value) {
		if (null == value) {
			return (new BigDecimal(0.0D));
		} else {
			return (value.getBigDecimalValue());
		}
	}

	private void assertDefined() {
		if (null != m_invalidValue) {
			throw new NumberFormatException(
					"Attempting to use a Money that was not properly initialized.  Invalid value is: "
							+ m_invalidValue);
		}
	}
}
